import tkinter as tk
import ttkbootstrap as ttk
from tkinter import filedialog, messagebox, simpledialog, scrolledtext
import os
import shutil
import time
import json
from PIL import Image, ImageTk, ExifTags, ImageDraw
import threading
from name_classifier import NameClassifier
from baidu_classifier import BaiduImageClassifier, BaiduAPIError
from person_detector import PersonDetector
import cv2
import numpy as np

DEFAULT_COARSE_MAPPING = {
    "人像": [
        "人像", "人物", "肖像", "合影", "自拍", "人脸", "人物照", "肖像照",
        "证件照", "写真", "婚纱", "新娘", "新郎", "情侣", "母子", "母女",
        "父子", "父女", "老人", "老年", "青年", "少女", "男生", "女生",
        "小孩", "儿童", "宝宝", "婴儿", "人物特写", "人物近景", "人物写真"
    ],
    "风景": [
        "风景", "景色", "风光", "山", "山峰", "雪山", "草地", "草原", "森林",
        "树林", "树", "大树", "树木", "植被", "绿地", "绿化", "花", "花朵",
        "花卉", "花海", "花园", "花坛", "湖", "湖泊", "河", "江", "溪",
        "泉", "瀑布", "海", "海洋", "海滩", "沙滩", "沙漠", "天空", "云",
        "云海", "日出", "日落", "黄昏", "黎明", "夜景", "城市", "都市",
        "街景", "建筑", "地标", "桥", "广场", "公园", "田野", "稻田", "梯田",
        "山谷", "峡谷", "丘陵", "村庄"
    ],
    "动物": ["动物", "宠物", "猫", "狗", "鸟", "马", "牛", "羊"],
    "美食": ["美食", "食物", "菜肴", "水果", "甜品", "饮料"],
    "物品": ["商品", "包", "鞋", "衣服", "家居", "电子产品"],
}

# 尝试导入HEIC支持
try:
    from pillow_heif import register_heif_opener
    register_heif_opener()
    HEIC_SUPPORT = True
except ImportError:
    HEIC_SUPPORT = False
    print("警告: 未安装pillow-heif，无法加载HEIC/HEIF格式图片")


class QuickPhotoClassifierApp:
    def __init__(self, root):
        self.root = root
        self.root.title("快速照片分类器")
        self.root.bind("<Configure>", self.on_window_resize)

        # 创建主题并设置颜色
        self.style = ttk.Style(theme="flatly")
        self.style.colors.primary = "#5C6BC0"  # 设置主色调为靛蓝色
        self.style.colors.secondary = "#5C6BC0"
        self.style.colors.success = "#66BB6A"
        self.style.colors.info = "#29B6F6"
        self.style.colors.warning = "#FFA726"
        self.style.colors.danger = "#EF5350"

        # 设置全局颜色
        self.style.configure(".", background="white", foreground="black")

        # 设置默认字体
        self.default_font = "Microsoft YaHei UI"  # 微软雅黑UI
        self.default_font_fallback = "Arial"  # 备用字体

        # 存储数据
        self.photo_path = ""  # 照片源文件夹
        self.photos = []  # 存储所有照片的路径
        self.category_folders = {}  # 分类文件夹 {名称: 路径}
        self.current_photo_index = 0  # 当前显示的照片索引
        self.current_photo_path = ""  # 当前显示的照片路径

        # 操作历史记录（用于撤回功能）
        self.operation_history = []  # 存储操作历史

        # 图片缩放相关
        self.current_image = None  # 当前原始图片（已处理EXIF方向）
        self.zoom_scale = 1.0  # 当前缩放比例
        self.base_scale = 1.0  # 基础缩放比例（适应窗口的比例）
        self.display_max_width = 900  # 显示区域最大宽度
        self.display_max_height = 500  # 显示区域最大高度

        # 视图模式
        self.view_mode = "single"  # single: 单图模式, grid: 网格模式
        self.selected_photos = set()  # 网格模式下选中的照片索引集合
        self.grid_photo_widgets = []  # 网格模式下的照片控件列表
        self.grid_item_width = 270  # 每个网格项的宽度（240缩略图+边距）
        self.last_grid_width = 0  # 记录上次网格布局时的宽度

        # 初始化名字分类器（提高默认阈值，使分类更精确）
        self.name_classifier = NameClassifier(similarity_threshold=0.95)

        # 初始化人体检测器
        self.person_detector = None
        self.person_classify_thread = None
        self.person_confidence_threshold = 0.45  # 默认置信度
        self.person_confidence_var = tk.DoubleVar(value=self.person_confidence_threshold)
        try:
            # 置信度阈值：0.4-0.5 更准确，0.3 平衡，0.25 宽松
            self.person_detector = PersonDetector(confidence_threshold=self.person_confidence_threshold)
        except Exception as e:
            print(f"初始化人体检测器失败: {e}")

        # 百度AI分类配置
        self.baidu_classifier = None
        self.baidu_config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "baidu_ai_config.json")
        self.baidu_min_score = 0.6
        self.baidu_allow_unclassified = True
        self.baidu_use_coarse_categories = False
        self.baidu_coarse_mapping = self.get_default_coarse_mapping()
        self.baidu_classify_thread = None
        self.baidu_api_key = ""
        self.baidu_secret_key = ""
        self.baidu_min_score_var = tk.DoubleVar(value=self.baidu_min_score)
        self.baidu_coarse_var = tk.BooleanVar(value=self.baidu_use_coarse_categories)
        self.load_baidu_api_config()

        # 创建主布局
        self.main_frame = ttk.Frame(self.root, padding="20")
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # 创建主工作区域（左侧照片区+右侧分类区）
        self.work_frame = ttk.Frame(self.main_frame)
        self.work_frame.pack(fill=tk.BOTH, expand=True)

        # 创建照片显示区域（左侧）
        self.create_photo_area()

        # 创建分类文件夹区域（右侧）
        self.create_category_area()

        # 状态栏
        self.status_var = tk.StringVar(value="准备就绪，请加载照片文件夹")
        self.status_bar = ttk.Label(self.main_frame, textvariable=self.status_var,
                                    relief=tk.SUNKEN, anchor=tk.W, font=(self.default_font, 9))
        self.status_bar.pack(fill=tk.X, side=tk.BOTTOM, pady=(5, 0))

        # 进度标签
        self.progress_label = ttk.Label(
            self.main_frame,
            text="",
            font=(self.default_font, 9),
            anchor=tk.E,
            padding=(10, 5)
        )
        self.progress_label.pack(side=tk.RIGHT)

    def create_photo_area(self):
        """创建照片显示区域"""
        # 左侧面板（照片显示）
        self.photo_panel = ttk.Frame(self.work_frame)
        self.photo_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # 照片区域标题和计数器
        header_frame = ttk.Frame(self.photo_panel)
        header_frame.pack(fill=tk.X, pady=(0, 5))

        self.photo_title = ttk.Label(
            header_frame,
            text="照片预览",
            font=(self.default_font, 12, "bold")
        )
        self.photo_title.pack(side=tk.LEFT)

        # 视图模式切换按钮
        self.view_mode_btn = ttk.Button(
            header_frame,
            text="网格视图",
            command=self.toggle_view_mode,
            bootstyle="info-outline",
            width=12
        )
        self.view_mode_btn.pack(side=tk.LEFT, padx=10)

        self.photo_counter = ttk.Label(
            header_frame,
            text="0/0",
            font=(self.default_font, 10)
        )
        self.photo_counter.pack(side=tk.RIGHT)

        # 照片显示区域
        self.photo_frame = ttk.Frame(self.photo_panel, padding=10)
        self.photo_frame.pack(fill=tk.BOTH, expand=True)

        # === 单图模式容器 ===
        self.single_view_container = ttk.Frame(self.photo_frame)
        self.single_view_container.pack(fill=tk.BOTH, expand=True)

        # 照片显示标签
        self.photo_label = ttk.Label(self.single_view_container)
        self.photo_label.pack(anchor=tk.CENTER, expand=True)

        # 绑定鼠标滚轮事件到照片标签
        self.photo_label.bind("<MouseWheel>", self.on_mouse_wheel)
        # Linux系统的滚轮事件
        self.photo_label.bind("<Button-4>", lambda e: self.on_mouse_wheel(e, delta=120))
        self.photo_label.bind("<Button-5>", lambda e: self.on_mouse_wheel(e, delta=-120))

        # 缩放控制按钮（小巧的浮动按钮）
        zoom_control_frame = ttk.Frame(self.single_view_container)
        zoom_control_frame.place(relx=0.02, rely=0.02)  # 放在左上角

        # 缩小按钮
        self.zoom_out_btn = ttk.Button(
            zoom_control_frame,
            text="−",
            command=self.zoom_out,
            width=3,
            bootstyle="secondary-outline"
        )
        self.zoom_out_btn.pack(side=tk.LEFT, padx=2)

        # 缩放比例显示
        self.zoom_label = ttk.Label(
            zoom_control_frame,
            text="100%",
            width=6,
            anchor=tk.CENTER,
            font=(self.default_font, 9)
        )
        self.zoom_label.pack(side=tk.LEFT, padx=2)

        # 放大按钮
        self.zoom_in_btn = ttk.Button(
            zoom_control_frame,
            text="+",
            command=self.zoom_in,
            width=3,
            bootstyle="secondary-outline"
        )
        self.zoom_in_btn.pack(side=tk.LEFT, padx=2)

        # 重置按钮
        self.zoom_reset_btn = ttk.Button(
            zoom_control_frame,
            text="⟲",
            command=self.zoom_reset,
            width=3,
            bootstyle="info-outline"
        )
        self.zoom_reset_btn.pack(side=tk.LEFT, padx=2)

        # === 网格模式容器 ===
        self.grid_view_container = ttk.Frame(self.photo_frame)
        # 默认不显示网格容器

        # 网格控制栏（全选、取消全选等）- 先pack，占用底部空间
        grid_control_frame = ttk.Frame(self.grid_view_container)
        grid_control_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5)

        # 左侧按钮
        ttk.Button(
            grid_control_frame,
            text="全选",
            command=self.select_all_grid_photos,
            bootstyle="success-outline",
            width=10
        ).pack(side=tk.LEFT, padx=5)

        ttk.Button(
            grid_control_frame,
            text="取消全选",
            command=self.deselect_all_grid_photos,
            bootstyle="secondary-outline",
            width=10
        ).pack(side=tk.LEFT, padx=5)

        # 右侧选择计数
        self.grid_selection_label = ttk.Label(
            grid_control_frame,
            text="已选: 0张",
            font=(self.default_font, 10)
        )
        self.grid_selection_label.pack(side=tk.RIGHT, padx=10)

        # 创建可滚动的Canvas用于网格显示 - 控制栏之后pack
        self.grid_canvas = tk.Canvas(self.grid_view_container, highlightthickness=0)
        self.grid_scrollbar = ttk.Scrollbar(
            self.grid_view_container,
            orient="vertical",
            command=self.grid_canvas.yview
        )
        self.grid_canvas.configure(yscrollcommand=self.grid_scrollbar.set)

        self.grid_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.grid_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # 网格内容框架
        self.grid_content_frame = ttk.Frame(self.grid_canvas)
        self.grid_canvas_window = self.grid_canvas.create_window(
            (0, 0),
            window=self.grid_content_frame,
            anchor="nw"
        )

        # 绑定Canvas大小改变事件
        self.grid_content_frame.bind("<Configure>", self._on_grid_frame_configure)
        self.grid_canvas.bind("<Configure>", self._on_grid_canvas_configure)

        # 绑定鼠标滚轮到网格canvas和容器
        self._bind_mousewheel_to_grid(self.grid_canvas)
        self._bind_mousewheel_to_grid(self.grid_view_container)
        self._bind_mousewheel_to_grid(self.grid_content_frame)

        # 文件名标签
        self.file_name_label = ttk.Label(
            self.photo_panel,
            font=(self.default_font, 10),
            anchor=tk.CENTER
        )
        self.file_name_label.pack(pady=(5, 10))

        # 导航按钮
        nav_frame = ttk.Frame(self.photo_panel)
        nav_frame.pack(fill=tk.X, pady=10)

        self.prev_btn = ttk.Button(
            nav_frame,
            text="← 上一张",
            command=self.show_previous_photo,
            bootstyle="secondary-outline"
        )
        self.prev_btn.pack(side=tk.LEFT, padx=10)

        # 撤回按钮（放在左侧）
        self.undo_btn = ttk.Button(
            nav_frame,
            text="↶ 撤回 (Ctrl+Z)",
            command=self.undo_last_operation,
            bootstyle="warning-outline"
        )
        self.undo_btn.pack(side=tk.LEFT, padx=(0, 10))

        # 键盘快捷键说明
        keyboard_hint = ttk.Label(
            nav_frame,
            text="左右方向键: 导航 | 数字键1-9: 快速分类 | Ctrl+Z: 撤回 | 鼠标滚轮: 缩放",
            font=(self.default_font, 10, "bold"),
            foreground="#333333"
        )
        keyboard_hint.pack(side=tk.LEFT, expand=True)

        self.next_btn = ttk.Button(
            nav_frame,
            text="下一张 →",
            command=self.show_next_photo,
            bootstyle="secondary-outline"
        )
        self.next_btn.pack(side=tk.RIGHT, padx=10)

        # 绑定键盘事件
        self.root.bind("<Left>", lambda e: self.show_previous_photo())
        self.root.bind("<Right>", lambda e: self.show_next_photo())
        self.root.bind("<Control-z>", lambda e: self.undo_last_operation())
        self.root.bind("<Control-Z>", lambda e: self.undo_last_operation())

    def create_category_area(self):
        """创建分类文件夹区域"""
        # 设置宽度为屏幕的1/4
        panel_width = int(self.root.winfo_screenwidth() / 4)

        # 右侧面板（分类文件夹）
        self.category_panel = ttk.Frame(self.work_frame, width=panel_width)
        self.category_panel.pack(side=tk.RIGHT, fill=tk.BOTH, padx=(10, 0))
        self.category_panel.pack_propagate(False)  # 防止被内部组件尺寸影响

        # 控制按钮区域
        control_frame = ttk.Frame(self.category_panel)
        control_frame.pack(fill=tk.X, pady=(0, 15))

        # 加载照片按钮
        self.load_photos_btn = ttk.Button(
            control_frame,
            text="加载照片文件夹",
            command=self.load_photos,
            bootstyle="primary",
            width=15
        )
        self.load_photos_btn.pack(side=tk.TOP, pady=(0, 5), fill=tk.X)

        # 创建分类文件夹按钮
        self.create_category_btn = ttk.Button(
            control_frame,
            text="创建分类文件夹",
            command=self.create_category_folder,
            bootstyle="info",
            width=15
        )
        self.create_category_btn.pack(side=tk.TOP, pady=(0, 5), fill=tk.X)

        # 清除分类文件夹按钮
        self.clear_category_btn = ttk.Button(
            control_frame,
            text="清除分类文件夹",
            command=self.clear_category_folders,
            bootstyle="danger",
            width=15
        )
        self.clear_category_btn.pack(side=tk.TOP, pady=(0, 5), fill=tk.X)

        # 按名字自动分类按钮
        self.auto_classify_btn = ttk.Button(
            control_frame,
            text="按名字自动分类",
            command=self.auto_classify_by_name,
            bootstyle="success",
            width=15
        )
        self.auto_classify_btn.pack(side=tk.TOP, fill=tk.X)

        self.baidu_classify_btn = ttk.Button(
            control_frame,
            text="百度AI自动分类",
            command=self.auto_classify_by_baidu,
            bootstyle="warning",
            width=15
        )
        self.baidu_classify_btn.pack(side=tk.TOP, pady=(5, 0), fill=tk.X)

        # 人像识别分类按钮
        self.person_classify_btn = ttk.Button(
            control_frame,
            text="人像识别分类",
            command=self.auto_classify_by_person,
            bootstyle="danger",
            width=15
        )
        self.person_classify_btn.pack(side=tk.TOP, pady=(5, 0), fill=tk.X)

        # 人像识别设置面板
        person_settings_frame = ttk.Labelframe(
            self.category_panel,
            text="人像识别设置",
            padding="8"
        )
        person_settings_frame.pack(fill=tk.X, pady=(10, 10))

        # 置信度调整
        threshold_row = ttk.Frame(person_settings_frame)
        threshold_row.pack(fill=tk.X, pady=2)
        ttk.Label(
            threshold_row,
            text="置信度阈值：",
            width=12
        ).pack(side=tk.LEFT)
        self.person_threshold_spinbox = ttk.Spinbox(
            threshold_row,
            from_=0.1,
            to=0.9,
            increment=0.05,
            textvariable=self.person_confidence_var,
            width=6
        )
        self.person_threshold_spinbox.pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(
            threshold_row,
            text="应用",
            command=self.apply_person_confidence,
            width=6
        ).pack(side=tk.LEFT)

        # 说明文字
        ttk.Label(
            person_settings_frame,
            text="值越大越严格（推荐0.4-0.5），减少误检",
            wraplength=220,
            font=(self.default_font, 9),
            foreground="#666666"
        ).pack(fill=tk.X, pady=(2, 0))

        # 快速调整按钮
        quick_adjust_frame = ttk.Frame(person_settings_frame)
        quick_adjust_frame.pack(fill=tk.X, pady=(5, 0))

        ttk.Label(
            quick_adjust_frame,
            text="快速选择：",
            font=(self.default_font, 9)
        ).pack(side=tk.LEFT, padx=(0, 5))

        ttk.Button(
            quick_adjust_frame,
            text="宽松(0.3)",
            command=lambda: self.set_person_confidence(0.30),
            bootstyle="info-outline",
            width=8
        ).pack(side=tk.LEFT, padx=2)

        ttk.Button(
            quick_adjust_frame,
            text="平衡(0.4)",
            command=lambda: self.set_person_confidence(0.40),
            bootstyle="success-outline",
            width=8
        ).pack(side=tk.LEFT, padx=2)

        ttk.Button(
            quick_adjust_frame,
            text="严格(0.5)",
            command=lambda: self.set_person_confidence(0.50),
            bootstyle="warning-outline",
            width=8
        ).pack(side=tk.LEFT, padx=2)

        baidu_settings_frame = ttk.Labelframe(
            self.category_panel,
            text="百度识别设置",
            padding="8"
        )
        baidu_settings_frame.pack(fill=tk.X, pady=(10, 10))

        threshold_row = ttk.Frame(baidu_settings_frame)
        threshold_row.pack(fill=tk.X, pady=2)
        ttk.Label(
            threshold_row,
            text="最低置信度：",
            width=12
        ).pack(side=tk.LEFT)
        self.baidu_threshold_spinbox = ttk.Spinbox(
            threshold_row,
            from_=0.0,
            to=1.0,
            increment=0.05,
            textvariable=self.baidu_min_score_var,
            width=6
        )
        self.baidu_threshold_spinbox.pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(
            threshold_row,
            text="应用",
            command=self.apply_baidu_min_score,
            width=6
        ).pack(side=tk.LEFT)

        self.baidu_coarse_check = ttk.Checkbutton(
            baidu_settings_frame,
            text="启用粗分类",
            variable=self.baidu_coarse_var,
            command=self.toggle_baidu_coarse_mode,
            bootstyle="round-toggle"
        )
        self.baidu_coarse_check.pack(anchor=tk.W, pady=(5, 0))

        ttk.Label(
            baidu_settings_frame,
            text="勾选后将根据常见关键词汇总为人像/风景等大类",
            wraplength=220,
            font=(self.default_font, 9),
            foreground="#666666"
        ).pack(fill=tk.X, pady=(2, 0))

        # 分类区域标题
        self.category_title = ttk.Label(
            self.category_panel,
            text="分类文件夹",
            font=(self.default_font, 12, "bold")
        )
        self.category_title.pack(anchor=tk.W, pady=(0, 5))

        # 添加点击提示标签
        self.category_hint = ttk.Label(
            self.category_panel,
            text="点击按钮直接分类照片",
            font=(self.default_font, 10, "bold"),
            foreground="#333333"
        )
        self.category_hint.pack(anchor=tk.W, pady=(0, 10))

        # 创建分类文件夹按钮区域
        self.category_buttons_frame = ttk.Frame(self.category_panel)
        self.category_buttons_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        # 说明标签
        self.category_tip = ttk.Label(
            self.category_panel,
            text="点击对应分类按钮，照片会立即移动并显示下一张",
            wraplength=panel_width - 20,
            justify=tk.LEFT,
            font=(self.default_font, 9),
            foreground="gray"
        )
        self.category_tip.pack(side=tk.BOTTOM, fill=tk.X, pady=5)

    def create_control_area(self):
        """创建顶部控制区域 - 已移除，控制按钮移至右侧面板"""
        pass

    def load_photos(self):
        """加载照片文件夹"""
        self.photo_path = filedialog.askdirectory(title="选择照片所在文件夹")

        if not self.photo_path:
            return

        # 清空之前的照片
        self.photos = []
        self.current_photo_index = 0

        # 清空网格模式下的选中状态
        self.selected_photos.clear()

        # 清空之前的分类文件夹列表
        self.category_folders = {}
        self.update_category_buttons()

        # 更新状态
        self.status_var.set(f"正在加载照片，请稍候...")
        self.root.update()

        # 在新线程中加载照片
        threading.Thread(target=self._load_photos_thread).start()

    def _load_photos_thread(self):
        """在线程中加载照片"""
        try:
            # 扫描并加载已存在的分类文件夹
            self.scan_existing_folders()

            # 获取文件夹中的所有图片文件
            for file in os.listdir(self.photo_path):
                file_path = os.path.join(self.photo_path, file)
                # 检查是否为图像文件
                if os.path.isfile(file_path) and self.is_image_file(file_path):
                    self.photos.append(file_path)

            # 更新UI
            if self.photos:
                # 根据当前视图模式更新显示
                if self.view_mode == "grid":
                    # 网格模式：刷新网格显示
                    self.root.after(0, self.show_grid_photos)
                else:
                    # 单图模式：显示第一张照片
                    self.root.after(0, self.show_current_photo)
                self.root.after(0, lambda: self.status_var.set(f"已加载 {len(self.photos)} 张照片"))
            else:
                self.root.after(0, lambda: self.status_var.set("未找到图片文件"))
                self.root.after(0, lambda: messagebox.showinfo("提示", "所选文件夹中没有发现图片文件"))

        except Exception as e:
            self.root.after(0, lambda: messagebox.showerror("错误", f"加载照片时出错: {str(e)}"))
            self.root.after(0, lambda: self.status_var.set("加载照片失败"))

    def scan_existing_folders(self):
        """扫描照片目录中已存在的文件夹，将其添加为分类文件夹"""
        try:
            # 遍历照片文件夹中的所有项目
            for item in os.listdir(self.photo_path):
                item_path = os.path.join(self.photo_path, item)
                # 如果是文件夹，添加到分类文件夹列表中
                if os.path.isdir(item_path) and item_path not in self.category_folders.values():
                    self.category_folders[item] = item_path
            
            # 更新分类文件夹按钮
            self.root.after(0, self.update_category_buttons)
        except Exception as e:
            print(f"扫描已存在文件夹时出错: {str(e)}")

    def is_image_file(self, file_path):
        """检查文件是否为可预览的图像或视频文件

        只识别可以在界面上预览显示的格式，不包括RAW格式。
        RAW文件会在移动JPG时自动跟随移动，无需单独识别。

        支持的格式包括：
        - 常规照片：JPG, JPEG, PNG, GIF, BMP, TIFF, WEBP
        - 现代格式：HEIC, HEIF, AVIF, JXL
        - 其他格式：ICO, PPM, PGM, PBM, PNM
        - 视频格式：MP4, AVI, MOV, MKV, WMV, FLV, WEBM, M4V, 3GP
        """
        try:
            file_ext = os.path.splitext(file_path)[1].lower()

            # 可预览的常规图片格式
            previewable_formats = [
                # JPEG系列
                '.jpg', '.jpeg', '.jpe', '.jfif',
                # PNG
                '.png',
                # GIF
                '.gif',
                # BMP
                '.bmp', '.dib',
                # TIFF
                '.tiff', '.tif',
                # WebP
                '.webp',
                # 其他
                '.ico',
                '.ppm', '.pgm', '.pbm', '.pnm',  # Netpbm
            ]

            # 需要特殊支持的格式
            special_formats = [
                '.heic', '.heif',  # Apple HEIF格式
                '.avif',  # AVIF
                '.jxl',   # JPEG XL
            ]

            # 视频格式
            video_formats = [
                '.mp4', '.avi', '.mov', '.mkv', '.wmv',
                '.flv', '.webm', '.m4v', '.3gp', '.mpg',
                '.mpeg', '.m2v', '.m4p', '.ogv', '.ts'
            ]

            if file_ext in previewable_formats:
                # 尝试用PIL打开验证（确保文件没有损坏）
                try:
                    with Image.open(file_path) as img:
                        img.verify()
                    return True
                except:
                    return False
            elif file_ext in special_formats:
                # HEIC等特殊格式：如果没有相应的支持库，不识别为可加载的图片
                if file_ext in ['.heic', '.heif']:
                    return HEIC_SUPPORT  # 只有安装了pillow-heif才返回True
                else:
                    # 其他特殊格式也尝试验证
                    try:
                        with Image.open(file_path) as img:
                            img.verify()
                        return True
                    except:
                        return False
            elif file_ext in video_formats:
                # 视频文件直接返回True
                return True

            return False
        except Exception:
            return False

    def get_exif_data(self, image):
        """安全地提取EXIF信息，兼容不同Pillow版本"""
        exif = None

        if hasattr(image, "getexif"):
            try:
                exif = image.getexif()
            except Exception:
                exif = None

        if not exif:
            getexif = getattr(image, "_getexif", None)
            if callable(getexif):
                try:
                    exif = getexif()
                except Exception:
                    exif = None

        if not exif:
            return {}

        if hasattr(exif, "items"):
            try:
                return dict(exif.items())
            except Exception:
                pass

        return exif or {}

    def is_video_file(self, file_path):
        """判断文件是否为视频文件"""
        try:
            file_ext = os.path.splitext(file_path)[1].lower()
            video_formats = [
                '.mp4', '.avi', '.mov', '.mkv', '.wmv',
                '.flv', '.webm', '.m4v', '.3gp', '.mpg',
                '.mpeg', '.m2v', '.m4p', '.ogv', '.ts'
            ]
            return file_ext in video_formats
        except Exception:
            return False

    def get_video_first_frame(self, video_path):
        """提取视频的第一帧作为PIL Image对象

        Returns:
            PIL.Image or None: 成功返回第一帧图像，失败返回None
        """
        try:
            # 打开视频文件
            cap = cv2.VideoCapture(video_path)

            if not cap.isOpened():
                return None

            # 读取第一帧
            ret, frame = cap.read()
            cap.release()

            if not ret or frame is None:
                return None

            # 将BGR转换为RGB
            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

            # 转换为PIL Image
            pil_image = Image.fromarray(frame_rgb)

            return pil_image

        except Exception as e:
            print(f"提取视频第一帧失败: {str(e)}")
            return None

    def add_play_icon_overlay(self, image):
        """在图像上添加半透明的播放图标覆盖层

        Args:
            image: PIL Image对象

        Returns:
            PIL Image对象，添加了播放图标
        """
        # 确保图像是RGBA模式
        if image.mode != 'RGBA':
            image = image.convert('RGBA')

        # 创建一个高分辨率的覆盖层用于抗锯齿
        scale = 4  # 放大倍数，用于抗锯齿
        overlay_size = (image.size[0] * scale, image.size[1] * scale)
        overlay = Image.new('RGBA', overlay_size, (0, 0, 0, 0))
        draw = ImageDraw.Draw(overlay)

        # 计算中心位置和图标尺寸（放大）
        width, height = overlay_size
        center_x, center_y = width // 2, height // 2
        circle_radius = min(width, height) // 3.5  # 圆形半径

        # 绘制半透明的深色圆形背景（更透明）
        circle_bbox = [
            center_x - circle_radius,
            center_y - circle_radius,
            center_x + circle_radius,
            center_y + circle_radius
        ]
        draw.ellipse(circle_bbox, fill=(0, 0, 0, 100))  # 黑色，透明度降低到100/255

        # 绘制白色圆形边框（根据图片尺寸自适应宽度）
        # 小图用更细的边框，大图用稍粗的边框
        base_width = min(image.size)
        if base_width <= 250:  # 小图（缩略图）
            border_width = int(1.8 * scale)  # 更细
        else:  # 大图
            border_width = int(2.5 * scale)

        draw.ellipse(circle_bbox, outline=(255, 255, 255, 220), width=border_width)

        # 绘制白色播放三角形
        triangle_size = circle_radius * 0.55
        offset_x = triangle_size * 0.1
        triangle_points = [
            (center_x - triangle_size * 0.35 + offset_x, center_y - triangle_size * 0.65),  # 左上
            (center_x - triangle_size * 0.35 + offset_x, center_y + triangle_size * 0.65),  # 左下
            (center_x + triangle_size * 0.7 + offset_x, center_y)  # 右中
        ]
        draw.polygon(triangle_points, fill=(255, 255, 255, 255))  # 完全不透明的白色

        # 缩小到原始尺寸（抗锯齿）
        overlay = overlay.resize(image.size, Image.LANCZOS)

        # 合并覆盖层和原图
        result = Image.alpha_composite(image, overlay)

        # 转换回RGB
        if result.mode == 'RGBA':
            # 创建白色背景
            background = Image.new('RGB', result.size, (255, 255, 255))
            background.paste(result, mask=result.split()[3])  # 使用alpha通道作为遮罩
            return background

        return result

    def show_current_photo(self):
        """显示当前索引的照片或视频第一帧"""
        if not self.photos or self.current_photo_index >= len(self.photos):
            self.photo_label.configure(image="")
            self.file_name_label.configure(text="无照片可显示")
            self.photo_counter.configure(text="0/0")
            self.status_var.set("无照片可显示")
            self.progress_label.configure(text="")
            self.current_image = None
            return

        # 获取当前照片路径
        self.current_photo_path = self.photos[self.current_photo_index]
        file_name = os.path.basename(self.current_photo_path)

        try:
            # 判断是视频还是图片
            is_video = self.is_video_file(self.current_photo_path)
            if is_video:
                # 提取视频第一帧
                image = self.get_video_first_frame(self.current_photo_path)
                if image is None:
                    raise Exception("无法提取视频第一帧")
            else:
                # 加载图片
                with Image.open(self.current_photo_path) as img:
                    # 加载图片数据到内存
                    img.load()
                    # 创建副本以确保文件句柄可以被释放
                    image = img.copy()

                # 处理照片方向问题（读取EXIF信息）
                try:
                    # 获取EXIF数据
                    exif = self.get_exif_data(image)
                    if exif:
                        # 查找方向标签
                        orientation_key = None
                        for key, value in ExifTags.TAGS.items():
                            if value == 'Orientation':
                                orientation_key = key
                                break

                        # 根据方向信息旋转图片
                        if orientation_key and orientation_key in exif:
                            orientation = exif[orientation_key]

                            if orientation == 2:
                                # 水平翻转
                                image = image.transpose(Image.FLIP_LEFT_RIGHT)
                            elif orientation == 3:
                                # 旋转180度
                                image = image.transpose(Image.ROTATE_180)
                            elif orientation == 4:
                                # 垂直翻转
                                image = image.transpose(Image.FLIP_TOP_BOTTOM)
                            elif orientation == 5:
                                # 垂直翻转后顺时针旋转90度
                                image = image.transpose(Image.FLIP_TOP_BOTTOM).transpose(Image.ROTATE_90)
                            elif orientation == 6:
                                # 顺时针旋转90度
                                image = image.transpose(Image.ROTATE_270)
                            elif orientation == 7:
                                # 水平翻转后顺时针旋转90度
                                image = image.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_270)
                            elif orientation == 8:
                                # 逆时针旋转90度
                                image = image.transpose(Image.ROTATE_90)
                except Exception as e:
                    # 如果读取EXIF失败，继续使用原始图像
                    print(f"读取EXIF信息失败: {str(e)}")

            # 如果是视频，添加播放图标覆盖层
            if is_video:
                image = self.add_play_icon_overlay(image)

            # 保存处理后的原始图片
            self.current_image = image

            # 计算基础缩放比例（适应窗口）
            width, height = image.size
            if width > self.display_max_width or height > self.display_max_height:
                self.base_scale = min(self.display_max_width / width, self.display_max_height / height)
            else:
                self.base_scale = 1.0

            # 重置缩放比例
            self.zoom_scale = 1.0

            # 渲染图片
            self.render_image()

            # 更新文件名和计数器（视频文件添加标识）
            if is_video:
                self.file_name_label.configure(text=f"🎬 {file_name}")
            else:
                self.file_name_label.configure(text=file_name)
            self.photo_counter.configure(text=f"{self.current_photo_index + 1}/{len(self.photos)}")

            # 更新状态栏和进度
            self.status_var.set(f"当前显示: {file_name}")
            progress_pct = int((self.current_photo_index + 1) / len(self.photos) * 100)
            self.progress_label.configure(text=f"处理进度: {progress_pct}%")

            # 绑定数字键1-9到前9个分类按钮
            for i in range(1, 10):
                self.root.bind(str(i), lambda e, idx=i - 1: self.handle_number_key(idx))

        except Exception as e:
            self.photo_label.configure(image="")
            self.file_name_label.configure(text=f"无法加载: {file_name}")
            self.status_var.set(f"错误: {str(e)}")
            self.current_image = None

    def render_image(self):
        """根据当前缩放比例渲染图片"""
        if self.current_image is None:
            return

        # 计算最终缩放比例
        final_scale = self.base_scale * self.zoom_scale

        # 计算新的尺寸
        width, height = self.current_image.size
        new_width = int(width * final_scale)
        new_height = int(height * final_scale)

        # 缩放图像
        if new_width > 0 and new_height > 0:
            resized_image = self.current_image.resize((new_width, new_height), Image.LANCZOS)

            # 转换为Tkinter可显示的格式
            photo = ImageTk.PhotoImage(resized_image)
            self.photo_label.configure(image=photo)
            self.photo_label.image = photo  # 防止被垃圾回收

            # 更新缩放比例显示
            zoom_percent = int(self.zoom_scale * 100)
            self.zoom_label.configure(text=f"{zoom_percent}%")

    def on_mouse_wheel(self, event, delta=None):
        """处理鼠标滚轮事件进行缩放"""
        if self.current_image is None:
            return

        # 获取滚轮方向
        if delta is None:
            delta = event.delta

        # 计算缩放步长
        zoom_step = 0.1

        if delta > 0:
            # 向上滚动，放大
            self.zoom_scale = min(self.zoom_scale + zoom_step, 5.0)  # 最大放大5倍
        else:
            # 向下滚动，缩小
            self.zoom_scale = max(self.zoom_scale - zoom_step, 0.1)  # 最小缩小到0.1倍

        # 重新渲染图片
        self.render_image()

    def zoom_in(self):
        """放大图片"""
        if self.current_image is None:
            return

        self.zoom_scale = min(self.zoom_scale + 0.2, 5.0)
        self.render_image()

    def zoom_out(self):
        """缩小图片"""
        if self.current_image is None:
            return

        self.zoom_scale = max(self.zoom_scale - 0.2, 0.1)
        self.render_image()

    def zoom_reset(self):
        """重置缩放"""
        if self.current_image is None:
            return

        self.zoom_scale = 1.0
        self.render_image()

    def toggle_view_mode(self):
        """切换视图模式"""
        if self.view_mode == "single":
            # 切换到网格模式
            self.view_mode = "grid"
            self.single_view_container.pack_forget()
            self.grid_view_container.pack(fill=tk.BOTH, expand=True)
            self.view_mode_btn.configure(text="单图视图")
            self.show_grid_photos()
        else:
            # 切换到单图模式
            self.view_mode = "single"
            self.grid_view_container.pack_forget()
            self.single_view_container.pack(fill=tk.BOTH, expand=True)
            self.view_mode_btn.configure(text="网格视图")
            self.selected_photos.clear()
            self.show_current_photo()

    def show_grid_photos(self):
        """显示网格照片 - 自适应布局"""
        # 清空之前的网格
        for widget in self.grid_content_frame.winfo_children():
            widget.destroy()
        self.grid_photo_widgets.clear()

        if not self.photos:
            ttk.Label(
                self.grid_content_frame,
                text="无照片可显示",
                font=(self.default_font, 12)
            ).pack(pady=50)
            return

        # 更新界面确保Canvas有正确的宽度
        self.grid_canvas.update_idletasks()
        canvas_width = self.grid_canvas.winfo_width()

        # 记录当前宽度
        self.last_grid_width = canvas_width

        # 计算每行能放几张照片
        if canvas_width > 100:  # 确保canvas已经有有效宽度
            cols_per_row = max(1, canvas_width // self.grid_item_width)
        else:
            cols_per_row = 3  # 默认值

        # 使用grid布局显示所有照片
        for idx, photo_path in enumerate(self.photos):
            row = idx // cols_per_row
            col = idx % cols_per_row
            self._create_grid_photo_item_grid(self.grid_content_frame, idx, row, col)

        # 更新计数器
        self.photo_counter.configure(text=f"共 {len(self.photos)} 张")

        # 强制更新布局和滚动区域
        self.grid_content_frame.update_idletasks()
        self.grid_canvas.configure(scrollregion=self.grid_canvas.bbox("all"))
        # 滚动到顶部
        self.grid_canvas.yview_moveto(0)

    def _delayed_grid_refresh(self):
        """延迟刷新网格（仅在宽度显著改变时）"""
        if not self.photos or self.view_mode != "grid":
            return

        # 获取当前宽度
        current_width = self.grid_canvas.winfo_width()

        # 计算宽度改变导致的列数变化
        old_cols = max(1, self.last_grid_width // self.grid_item_width)
        new_cols = max(1, current_width // self.grid_item_width)

        # 只有当列数真的改变时才重新布局
        if old_cols != new_cols:
            self.show_grid_photos()

    def _create_grid_photo_item_grid(self, parent, photo_index, row, col):
        """创建单个网格照片项（使用grid布局）"""
        photo_path = self.photos[photo_index]
        file_name = os.path.basename(photo_path)
        is_video = self.is_video_file(photo_path)

        # 照片项容器 - 视频使用深色背景
        if is_video:
            item_frame = tk.Frame(parent, relief=tk.RAISED, borderwidth=2, bg="#2C3E50")
        else:
            item_frame = ttk.Frame(parent, relief=tk.RAISED, borderwidth=1)
        item_frame.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")

        # 复选框变量
        is_selected = tk.BooleanVar(value=photo_index in self.selected_photos)

        # 复选框
        checkbox_frame = tk.Frame(item_frame, bg="#2C3E50" if is_video else None)
        checkbox_frame.pack(anchor=tk.NW, padx=5, pady=5)

        checkbox = ttk.Checkbutton(
            checkbox_frame,
            variable=is_selected,
            command=lambda: self._toggle_photo_selection(photo_index, is_selected.get()),
            bootstyle="success-round-toggle"
        )
        checkbox.pack(side=tk.LEFT)

        # 视频标识
        if is_video:
            video_indicator = tk.Label(
                checkbox_frame,
                text="🎬",
                font=(self.default_font, 12),
                bg="#2C3E50",
                fg="white"
            )
            video_indicator.pack(side=tk.LEFT, padx=5)

        # 缩略图
        photo_label = None
        try:
            if is_video:
                # 提取视频第一帧
                image = self.get_video_first_frame(photo_path)
                if image is None:
                    raise Exception("无法提取视频第一帧")
            else:
                # 加载图片
                with Image.open(photo_path) as img:
                    # 加载图片数据到内存
                    img.load()
                    # 创建副本以确保文件句柄可以被释放
                    image = img.copy()

                # 处理EXIF方向
                try:
                    exif = self.get_exif_data(image)
                    if exif:
                        for key, value in ExifTags.TAGS.items():
                            if value == 'Orientation':
                                orientation_key = key
                                break
                        if orientation_key in exif:
                            orientation = exif[orientation_key]
                            if orientation == 3:
                                image = image.transpose(Image.ROTATE_180)
                            elif orientation == 6:
                                image = image.transpose(Image.ROTATE_270)
                            elif orientation == 8:
                                image = image.transpose(Image.ROTATE_90)
                except:
                    pass

            # 创建缩略图
            thumb_size = 240
            image.thumbnail((thumb_size, thumb_size), Image.LANCZOS)

            # 如果是视频，添加播放图标覆盖层
            if is_video:
                image = self.add_play_icon_overlay(image)

            photo = ImageTk.PhotoImage(image)

            # 创建缩略图标签
            if is_video:
                photo_label = tk.Label(item_frame, image=photo, bg="#2C3E50", cursor="hand2")
            else:
                photo_label = ttk.Label(item_frame, image=photo)

            photo_label.image = photo  # 防止垃圾回收
            photo_label.pack(padx=5, pady=5)

            # 点击图片也可以选择
            photo_label.bind("<Button-1>", lambda e: self._click_photo_item(checkbox, is_selected))
            self._bind_mousewheel_to_grid(photo_label)

        except Exception as e:
            if is_video:
                error_label = tk.Label(
                    item_frame,
                    text="视频加载失败",
                    font=(self.default_font, 10),
                    fg="red",
                    bg="#2C3E50"
                )
            else:
                error_label = ttk.Label(
                    item_frame,
                    text="加载失败",
                    font=(self.default_font, 10),
                    foreground="red"
                )
            error_label.pack(padx=5, pady=5)
            self._bind_mousewheel_to_grid(error_label)

        # 文件名
        display_name = file_name if len(file_name) <= 20 else file_name[:17] + "..."
        if is_video:
            name_label = tk.Label(
                item_frame,
                text=display_name,
                font=(self.default_font, 9),
                anchor=tk.CENTER,
                wraplength=240,
                bg="#2C3E50",
                fg="white"
            )
        else:
            name_label = ttk.Label(
                item_frame,
                text=display_name,
                font=(self.default_font, 9),
                anchor=tk.CENTER,
                wraplength=240
            )
        name_label.pack(padx=5, pady=5)

        # 绑定鼠标滚轮事件到所有子控件
        self._bind_mousewheel_to_grid(item_frame)
        self._bind_mousewheel_to_grid(checkbox_frame)
        self._bind_mousewheel_to_grid(checkbox)
        self._bind_mousewheel_to_grid(name_label)
        if is_video and 'video_indicator' in locals():
            self._bind_mousewheel_to_grid(video_indicator)

        # 保存控件引用
        self.grid_photo_widgets.append({
            'index': photo_index,
            'frame': item_frame,
            'checkbox_var': is_selected,
            'checkbox': checkbox
        })

    def _create_grid_photo_item(self, parent, photo_index):
        """创建单个网格照片项（废弃，保留以防兼容性）"""
        pass

    def _click_photo_item(self, checkbox, is_selected_var):
        """点击照片项切换选择状态"""
        checkbox.invoke()

    def _toggle_photo_selection(self, photo_index, is_selected):
        """切换照片选择状态"""
        if is_selected:
            self.selected_photos.add(photo_index)
        else:
            self.selected_photos.discard(photo_index)

        # 更新选择计数
        self.grid_selection_label.configure(text=f"已选: {len(self.selected_photos)}张")

    def select_all_grid_photos(self):
        """全选当前网格中的照片"""
        for widget_info in self.grid_photo_widgets:
            photo_index = widget_info['index']
            self.selected_photos.add(photo_index)
            widget_info['checkbox_var'].set(True)

        self.grid_selection_label.configure(text=f"已选: {len(self.selected_photos)}张")

    def deselect_all_grid_photos(self):
        """取消全选"""
        self.selected_photos.clear()
        for widget_info in self.grid_photo_widgets:
            widget_info['checkbox_var'].set(False)

        self.grid_selection_label.configure(text="已选: 0张")

    def _on_grid_frame_configure(self, event=None):
        """网格框架配置改变时更新滚动区域"""
        self.grid_canvas.configure(scrollregion=self.grid_canvas.bbox("all"))

    def _on_grid_canvas_configure(self, event):
        """Canvas大小改变时调整内部框架宽度"""
        self.grid_canvas.itemconfig(self.grid_canvas_window, width=event.width)

        # 窗口调整大小时延迟重新布局，避免拖动时卡顿
        if self.view_mode == "grid" and self.photos:
            # 取消之前的定时器
            if hasattr(self, '_resize_timer'):
                self.root.after_cancel(self._resize_timer)
            # 延迟800ms后重新布局（用户停止拖动后才触发）
            self._resize_timer = self.root.after(800, self._delayed_grid_refresh)

    def _on_grid_mousewheel(self, event, delta=None):
        """网格视图鼠标滚轮滚动"""
        if delta is None:
            delta = event.delta

        # 滚动canvas
        self.grid_canvas.yview_scroll(int(-1 * (delta / 120)), "units")

    def _bind_mousewheel_to_grid(self, widget):
        """绑定鼠标滚轮事件到指定控件"""
        widget.bind("<MouseWheel>", self._on_grid_mousewheel)
        # Linux系统的滚轮事件
        widget.bind("<Button-4>", lambda e: self._on_grid_mousewheel(e, delta=120))
        widget.bind("<Button-5>", lambda e: self._on_grid_mousewheel(e, delta=-120))

    def handle_number_key(self, index):
        """处理数字键1-9的快捷操作"""
        # 获取所有分类文件夹
        folders = list(self.category_folders.items())

        # 检查索引是否有效
        if 0 <= index < len(folders):
            name, path = folders[index]
            self.select_category(path, name)

    def show_next_photo(self):
        """显示下一张照片"""
        if not self.photos:
            return

        if self.current_photo_index < len(self.photos) - 1:
            self.current_photo_index += 1
            self.show_current_photo()
        else:
            messagebox.showinfo("提示", "已经是最后一张照片")

    def show_previous_photo(self):
        """显示上一张照片"""
        if not self.photos:
            return

        if self.current_photo_index > 0:
            self.current_photo_index -= 1
            self.show_current_photo()
        else:
            messagebox.showinfo("提示", "已经是第一张照片")

    def create_category_folder(self):
        """创建新的分类文件夹"""
        # 确保已选择照片源文件夹
        if not self.photo_path:
            messagebox.showwarning("警告", "请先加载照片文件夹！")
            return

        # 获取分类文件夹名称
        folder_name = simpledialog.askstring("创建分类文件夹", "请输入分类文件夹名称:")

        if not folder_name:
            return

        # 创建分类文件夹（直接在照片文件夹内创建）
        category_path = os.path.join(self.photo_path, folder_name)

        try:
            if not os.path.exists(category_path):
                os.makedirs(category_path)

            # 添加到分类文件夹列表
            self.category_folders[folder_name] = category_path
            self.update_category_buttons()

            messagebox.showinfo("成功", f"已在照片文件夹内创建分类文件夹: {folder_name}")

        except Exception as e:
            messagebox.showerror("错误", f"创建文件夹失败: {str(e)}")

    def update_category_buttons(self):
        """更新分类文件夹按钮"""
        # 清空按钮区域
        for widget in self.category_buttons_frame.winfo_children():
            widget.destroy()

        # 如果没有分类文件夹，显示提示
        if not self.category_folders:
            ttk.Label(
                self.category_buttons_frame,
                text="请先创建分类文件夹",
                foreground="gray",
                font=(self.default_font, 10)
            ).pack(pady=10)
            return

        # 添加分类文件夹按钮
        for i, (name, path) in enumerate(self.category_folders.items()):
            button_frame = ttk.Frame(self.category_buttons_frame)
            button_frame.pack(fill=tk.X, pady=5, padx=5)

            # 显示快捷键
            shortcut_text = f"{i + 1}. " if i < 9 else ""

            # 创建按钮
            category_btn = ttk.Button(
                button_frame,
                text=f"{shortcut_text}{name}",
                bootstyle="outline",
                command=lambda p=path, n=name: self.select_category(p, n)
            )

            # 设置不同颜色
            btn_styles = ["primary", "success", "info", "warning", "danger"]
            btn_style = btn_styles[i % len(btn_styles)]
            category_btn.configure(bootstyle=f"{btn_style}-outline")

            category_btn.pack(side=tk.LEFT, fill=tk.X, expand=True)

            # 添加删除按钮
            delete_btn = ttk.Button(
                button_frame,
                text="×",
                bootstyle="danger-outline",
                width=2,
                command=lambda n=name: self.remove_category_folder(n)
            )
            delete_btn.pack(side=tk.RIGHT, padx=(5, 0))

    def select_category(self, category_path, category_name):
        """选择分类文件夹，移动照片（单图模式：当前照片，网格模式：选中的照片）"""
        if not self.photos:
            messagebox.showwarning("警告", "没有照片可分类！")
            return

        # 网格模式：批量移动选中的照片
        if self.view_mode == "grid":
            if not self.selected_photos:
                messagebox.showwarning("警告", "请先选择要分类的照片！")
                return

            # 将选中的索引转换为照片路径列表（按索引降序排列，从后往前删除）
            selected_indices = sorted(self.selected_photos, reverse=True)
            photos_to_move = [self.photos[idx] for idx in selected_indices]

            # 批量操作记录
            batch_operation = {
                'is_batch': True,  # 标记为批量操作
                'operations': [],  # 存储所有单个操作
                'category_name': category_name,
                'count': 0
            }

            success_count = 0
            for idx, photo_path in zip(selected_indices, photos_to_move):
                move_result = self.move_photo(photo_path, category_path, category_name)

                if move_result:
                    # 记录单个操作到批量操作中
                    single_operation = {
                        'photo_path': photo_path,
                        'moved_files': move_result['moved_files'],
                        'target_folder': category_path,
                        'category_name': category_name,
                        'index': idx,
                        'display_names': move_result['display_names']
                    }
                    batch_operation['operations'].append(single_operation)

                    # 从照片列表中移除
                    self.photos.remove(photo_path)
                    success_count += 1

            # 只有成功移动了照片才记录历史
            if success_count > 0:
                batch_operation['count'] = success_count
                self.operation_history.append(batch_operation)

            # 清空选择
            self.selected_photos.clear()

            # 更新状态
            self.status_var.set(f"已将 {success_count} 张照片移动到 {category_name}")

            # 刷新网格显示
            if self.photos:
                # 调整当前索引
                if self.current_photo_index >= len(self.photos):
                    self.current_photo_index = max(0, len(self.photos) - 1)
                self.show_grid_photos()
            else:
                # 没有照片了
                messagebox.showinfo("完成", "所有照片已分类完成！")
                self.toggle_view_mode()  # 切换回单图模式

        # 单图模式：移动当前照片
        else:
            if self.current_photo_index >= len(self.photos):
                messagebox.showwarning("警告", "没有照片可分类！")
                return

            current_photo = self.current_photo_path
            move_result = self.move_photo(current_photo, category_path, category_name)

            if move_result:
                # 记录操作历史
                history_entry = {
                    'photo_path': current_photo,
                    'moved_files': move_result['moved_files'],
                    'target_folder': category_path,
                    'category_name': category_name,
                    'index': self.current_photo_index,
                    'display_names': move_result['display_names']
                }
                self.operation_history.append(history_entry)

                # 从照片列表中移除已移动的照片
                self.photos.remove(current_photo)

                # 如果还有照片，显示下一张
                if self.photos:
                    # 如果是最后一张，索引需要减1
                    if self.current_photo_index >= len(self.photos):
                        self.current_photo_index = len(self.photos) - 1
                    self.show_current_photo()
                else:
                    # 没有照片了
                    self.photo_label.configure(image="")
                    self.file_name_label.configure(text="所有照片已分类完成")
                    self.photo_counter.configure(text="0/0")
                    self.status_var.set("分类完成")
                    self.progress_label.configure(text="进度: 100%")

                    # 显示完成消息
                    messagebox.showinfo("完成", "所有照片已分类完成！")

    def move_photo(self, photo_path, target_folder, category_name):
        """移动照片到目标文件夹，同时移动对应的RAW文件

        返回:
            dict or None: 成功返回包含移动信息的字典，失败返回None
            {
                'moved_files': [(source_path, dest_path), ...],
                'display_names': [file_name, ...]
            }
        """
        try:
            # 获取文件名和扩展名
            file_name = os.path.basename(photo_path)
            base_name, ext = os.path.splitext(file_name)
            source_folder = os.path.dirname(photo_path)
            ext_lower = ext.lower()

            # 目标路径
            dest_path = os.path.join(target_folder, file_name)
            original_base_name = base_name

            # 如果目标文件已存在，添加后缀
            if os.path.exists(dest_path):
                i = 1
                while os.path.exists(dest_path):
                    new_file_name = f"{base_name}_{i}{ext}"
                    dest_path = os.path.join(target_folder, new_file_name)
                    i += 1

            # 移动主文件（带重试机制）
            max_retries = 3
            retry_delay = 0.5  # 秒
            last_error = None

            for attempt in range(max_retries):
                try:
                    # 强制垃圾回收，释放可能还未释放的文件句柄
                    import gc
                    gc.collect()

                    # 尝试移动文件
                    shutil.move(photo_path, dest_path)
                    break  # 成功则跳出重试循环
                except PermissionError as e:
                    last_error = e
                    if attempt < max_retries - 1:
                        # 还有重试机会，等待后重试
                        time.sleep(retry_delay)
                        retry_delay *= 2  # 指数退避
                    else:
                        # 最后一次尝试也失败了
                        raise Exception(f"文件被占用，无法移动: {os.path.basename(photo_path)}。\n可能需要稍后再试或重启程序。") from e

            moved_files_info = [(photo_path, dest_path)]  # 记录 (源路径, 目标路径)
            display_names = [file_name]

            # 如果是JPG文件，查找对应的RAW文件
            if ext_lower in ['.jpg', '.jpeg', '.jpe', '.jfif']:
                # 所有主流相机的RAW格式扩展名
                raw_extensions = [
                    '.raw',  # 通用RAW
                    '.cr2', '.cr3',  # Canon RAW
                    '.nef', '.nrw',  # Nikon RAW
                    '.arw', '.srf', '.sr2',  # Sony RAW
                    '.dng',  # Adobe Digital Negative
                    '.orf',  # Olympus RAW
                    '.rw2',  # Panasonic RAW
                    '.raf',  # Fujifilm RAW
                    '.rwl', '.rwz',  # Leica RAW
                    '.pef', '.ptx',  # Pentax RAW
                    '.srw',  # Samsung RAW
                    '.x3f',  # Sigma RAW
                    '.erf',  # Epson RAW
                    '.mrw',  # Minolta RAW
                    '.dcr', '.kdc',  # Kodak RAW
                    '.3fr',  # Hasselblad RAW
                    '.mef',  # Mamiya RAW
                    '.mos',  # Leaf RAW
                    '.bay',  # Casio RAW
                    '.cap', '.iiq',  # Phase One RAW
                    '.fff',  # Imacon RAW
                ]

                # 在同一文件夹中查找对应的RAW文件
                for raw_ext in raw_extensions:
                    raw_file = os.path.join(source_folder, f"{original_base_name}{raw_ext}")
                    raw_file_upper = os.path.join(source_folder, f"{original_base_name}{raw_ext.upper()}")

                    # 检查小写和大写扩展名
                    if os.path.exists(raw_file):
                        raw_file_name = os.path.basename(raw_file)
                        raw_dest_path = os.path.join(target_folder, raw_file_name)

                        # 检查目标位置是否存在同名文件
                        if os.path.exists(raw_dest_path):
                            base, r_ext = os.path.splitext(raw_file_name)
                            i = 1
                            while os.path.exists(raw_dest_path):
                                new_raw_name = f"{base}_{i}{r_ext}"
                                raw_dest_path = os.path.join(target_folder, new_raw_name)
                                i += 1

                        # 移动RAW文件（带重试机制）
                        for attempt in range(max_retries):
                            try:
                                import gc
                                gc.collect()
                                shutil.move(raw_file, raw_dest_path)
                                break
                            except PermissionError as e:
                                if attempt < max_retries - 1:
                                    time.sleep(0.5)
                                else:
                                    # RAW文件移动失败不应该阻止整个操作，只记录警告
                                    print(f"警告：无法移动RAW文件 {raw_file_name}: {str(e)}")
                                    break

                        moved_files_info.append((raw_file, raw_dest_path))
                        display_names.append(raw_file_name)

                    elif os.path.exists(raw_file_upper):
                        raw_file_name = os.path.basename(raw_file_upper)
                        raw_dest_path = os.path.join(target_folder, raw_file_name)

                        # 检查目标位置是否存在同名文件
                        if os.path.exists(raw_dest_path):
                            base, r_ext = os.path.splitext(raw_file_name)
                            i = 1
                            while os.path.exists(raw_dest_path):
                                new_raw_name = f"{base}_{i}{r_ext}"
                                raw_dest_path = os.path.join(target_folder, new_raw_name)
                                i += 1

                        # 移动RAW文件（带重试机制）
                        for attempt in range(max_retries):
                            try:
                                import gc
                                gc.collect()
                                shutil.move(raw_file_upper, raw_dest_path)
                                break
                            except PermissionError as e:
                                if attempt < max_retries - 1:
                                    time.sleep(0.5)
                                else:
                                    # RAW文件移动失败不应该阻止整个操作，只记录警告
                                    print(f"警告：无法移动RAW文件 {raw_file_name}: {str(e)}")
                                    break

                        moved_files_info.append((raw_file_upper, raw_dest_path))
                        display_names.append(raw_file_name)

            # 更新状态栏
            if len(display_names) > 1:
                moved_info = ", ".join(display_names[:2])
                if len(display_names) > 2:
                    moved_info += f" 等 {len(display_names)} 个文件"
                self.status_var.set(f"已将 {moved_info} 移动到 {category_name}")
            else:
                self.status_var.set(f"已将 {file_name} 移动到 {category_name}")

            return {
                'moved_files': moved_files_info,
                'display_names': display_names
            }

        except Exception as e:
            messagebox.showerror("错误", f"移动照片时发生错误：{str(e)}")
            return None

    def undo_last_operation(self):
        """撤回上一次分类操作（支持批量撤回）"""
        if not self.operation_history:
            messagebox.showinfo("提示", "没有可撤回的操作！")
            return

        # 获取最后一个操作记录
        last_operation = self.operation_history.pop()

        try:
            # 检查是否为批量操作
            if last_operation.get('is_batch', False):
                # 批量撤回
                undo_count = 0

                # 按相反顺序撤回所有操作
                for single_op in reversed(last_operation['operations']):
                    # 将所有文件移回原位置
                    for source_path, dest_path in reversed(single_op['moved_files']):
                        if os.path.exists(dest_path):
                            shutil.move(dest_path, source_path)
                        else:
                            raise FileNotFoundError(f"文件不存在: {dest_path}")

                    # 将照片重新加入到照片列表中
                    original_photo_path = single_op['photo_path']
                    original_index = single_op['index']

                    if original_index < len(self.photos):
                        self.photos.insert(original_index, original_photo_path)
                    else:
                        self.photos.append(original_photo_path)

                    undo_count += 1

                # 更新状态栏
                self.status_var.set(f"已撤回 {undo_count} 张照片从 {last_operation['category_name']}")

                # 根据当前模式刷新显示
                if self.view_mode == "grid":
                    self.show_grid_photos()
                else:
                    # 单图模式：显示最后一个撤回的照片
                    if self.photos:
                        self.current_photo_index = min(self.current_photo_index, len(self.photos) - 1)
                        self.show_current_photo()

            else:
                # 单个操作撤回（原有逻辑）
                # 将所有文件移回原位置
                for source_path, dest_path in reversed(last_operation['moved_files']):
                    if os.path.exists(dest_path):
                        shutil.move(dest_path, source_path)
                    else:
                        raise FileNotFoundError(f"文件不存在: {dest_path}")

                # 将照片重新加入到照片列表中
                original_photo_path = last_operation['photo_path']
                original_index = last_operation['index']

                if original_index < len(self.photos):
                    self.photos.insert(original_index, original_photo_path)
                    self.current_photo_index = original_index
                else:
                    self.photos.append(original_photo_path)
                    self.current_photo_index = len(self.photos) - 1

                # 显示撤回的照片
                self.show_current_photo()

                # 更新状态栏
                display_names = last_operation['display_names']
                if len(display_names) > 1:
                    files_info = ", ".join(display_names[:2])
                    if len(display_names) > 2:
                        files_info += f" 等 {len(display_names)} 个文件"
                else:
                    files_info = display_names[0]

                self.status_var.set(f"已撤回: {files_info} 从 {last_operation['category_name']}")

        except Exception as e:
            messagebox.showerror("错误", f"撤回操作时发生错误：{str(e)}")
            # 如果撤回失败，将操作记录放回历史中
            self.operation_history.append(last_operation)
            self.status_var.set("撤回失败")

    def remove_category_folder(self, category_name):
        """从列表中移除分类文件夹"""
        if category_name in self.category_folders:
            if messagebox.askyesno("确认", f"确定要从列表中移除 '{category_name}' 分类文件夹吗？\n(不会删除实际文件夹)"):
                del self.category_folders[category_name]
                self.update_category_buttons()
                self.status_var.set(f"已移除分类文件夹: {category_name}")

    def clear_category_folders(self):
        """清除所有分类文件夹"""
        if self.category_folders and messagebox.askyesno("确认", "确定要清除所有分类文件夹吗？\n(不会删除实际文件夹)"):
            self.category_folders = {}
            self.update_category_buttons()
            self.status_var.set("已清除所有分类文件夹")


    def load_baidu_api_config(self):
        """加载百度AI配置"""
        self.baidu_classifier = None
        config = {}
        self.baidu_api_key = ""
        self.baidu_secret_key = ""
        if os.path.exists(self.baidu_config_path):
            try:
                with open(self.baidu_config_path, "r", encoding="utf-8") as config_file:
                    config = json.load(config_file)
            except Exception as exc:
                print(f"加载百度AI配置失败: {exc}")
        else:
            self.baidu_coarse_mapping = self.get_default_coarse_mapping()

        api_key = str(config.get("api_key", "")).strip()
        secret_key = str(config.get("secret_key", "")).strip()
        self.baidu_api_key = api_key
        self.baidu_secret_key = secret_key
        try:
            self.baidu_min_score = float(config.get("min_score", self.baidu_min_score))
        except (TypeError, ValueError):
            self.baidu_min_score = 0.6
        self.baidu_allow_unclassified = bool(
            config.get("allow_unclassified", self.baidu_allow_unclassified)
        )
        self.baidu_use_coarse_categories = bool(
            config.get("use_coarse_categories", self.baidu_use_coarse_categories)
        )

        mapping_data = config.get("coarse_mapping")
        if isinstance(mapping_data, dict) and mapping_data:
            normalized = {}
            for coarse_name, keywords in mapping_data.items():
                name = str(coarse_name).strip()
                if not name:
                    continue
                normalized[name] = [
                    str(keyword).strip()
                    for keyword in keywords or []
                    if str(keyword).strip()
                ]
            if normalized:
                self.baidu_coarse_mapping = normalized
            else:
                self.baidu_coarse_mapping = self.get_default_coarse_mapping()
        elif not mapping_data:
            self.baidu_coarse_mapping = self.get_default_coarse_mapping()

        if hasattr(self, "baidu_min_score_var"):
            self.baidu_min_score_var.set(self.baidu_min_score)
        if hasattr(self, "baidu_coarse_var"):
            self.baidu_coarse_var.set(self.baidu_use_coarse_categories)

        if api_key and secret_key:
            try:
                self.baidu_classifier = BaiduImageClassifier(
                    api_key,
                    secret_key,
                    min_score=self.baidu_min_score,
                    allow_unclassified=self.baidu_allow_unclassified,
                )
            except Exception as exc:
                print(f"初始化百度AI分类器失败: {exc}")
        else:
            self.baidu_classifier = None

    def auto_classify_by_baidu(self):
        """调用百度AI进行智能分类"""
        if not self.photo_path:
            messagebox.showwarning("警告", "请先加载照片文件夹！")
            return

        if not self.photos:
            messagebox.showwarning("提示", "当前未加载任何照片，请先加载文件夹。")
            return

        self.load_baidu_api_config()
        if not self.baidu_classifier:
            messagebox.showwarning(
                "提示",
                f"请在 {self.baidu_config_path} 中配置百度AI的 API Key 和 Secret Key 后再试。",
            )
            return

        if self.baidu_classify_thread and self.baidu_classify_thread.is_alive():
            messagebox.showinfo("提示", "百度自动分类正在执行，请稍候完成当前任务。")
            return

        self.status_var.set("正在调用百度AI进行分类，请稍候...")
        self.progress_label.config(text="正在准备百度AI识别...")
        self.baidu_classify_thread = threading.Thread(
            target=self._run_baidu_classification,
            daemon=True
        )
        self.baidu_classify_thread.start()

    def auto_classify_by_person(self):
        """使用YOLOv8进行人像识别分类"""
        if not self.photo_path:
            messagebox.showwarning("警告", "请先加载照片文件夹！")
            return

        if not self.photos:
            messagebox.showwarning("提示", "当前未加载任何照片，请先加载文件夹。")
            return

        if not self.person_detector:
            messagebox.showwarning("提示", "人体检测器初始化失败，请检查是否已安装 ultralytics 库。")
            return

        if self.person_classify_thread and self.person_classify_thread.is_alive():
            messagebox.showinfo("提示", "人像识别正在执行，请稍候完成当前任务。")
            return

        # 确认对话框
        result = messagebox.askyesno(
            "确认分类",
            f"即将对 {len(self.photos)} 张照片进行人像识别分类。\n\n"
            "分类结果：\n"
            "• 有人 - 包含人物的照片\n"
            "• 无人 - 不包含人物的照片\n\n"
            f"当前置信度阈值: {self.person_confidence_threshold:.2f}\n"
            "（可在设置面板中调整）\n\n"
            "是否继续？",
            icon='question'
        )

        if not result:
            return

        self.status_var.set("正在使用YOLOv8进行人像识别，请稍候...")
        self.progress_label.config(text="正在准备YOLOv8模型...")
        self.person_classify_thread = threading.Thread(
            target=self._run_person_classification,
            daemon=True
        )
        self.person_classify_thread.start()

    def apply_person_confidence(self):
        """应用用户调整的置信度阈值"""
        try:
            value = float(self.person_confidence_var.get())
        except (tk.TclError, TypeError, ValueError):
            messagebox.showwarning("提示", "请输入0.1-0.9之间的数字")
            return

        value = max(0.1, min(0.9, value))
        self.person_confidence_var.set(value)
        self.person_confidence_threshold = value

        # 更新检测器的阈值
        if self.person_detector:
            self.person_detector.confidence_threshold = value

        self.status_var.set(f"人像识别置信度已更新为 {value:.2f}")

    def set_person_confidence(self, value):
        """快速设置置信度（供快速按钮使用）"""
        self.person_confidence_var.set(value)
        self.apply_person_confidence()


    def _run_person_classification(self):
        """在后台线程中运行人像识别"""
        def progress_callback(current, total, filename, has_person, person_count, confidence):
            status = "有人" if has_person else "无人"
            detail = f"(人数: {person_count}, 置信度: {confidence:.2f})" if has_person else ""
            self.root.after(
                0,
                lambda: self._update_person_progress(current, total, filename, status, detail)
            )

        try:
            # 执行分类
            with_person, without_person = self.person_detector.classify_photos(
                self.photos,
                progress_callback=progress_callback
            )

            # 在主线程中处理结果
            self.root.after(
                0,
                lambda: self._handle_person_classification_result(with_person, without_person)
            )

        except Exception as e:
            self.root.after(
                0,
                lambda: self._handle_person_classification_error(str(e))
            )

    def _update_person_progress(self, current, total, filename, status, detail):
        """更新人像识别进度"""
        percent = int((current / total) * 100) if total else 0
        self.progress_label.config(
            text=f"人像识别 {current}/{total} ({percent}%) - {filename} - {status} {detail}"
        )

    def _handle_person_classification_result(self, with_person, without_person):
        """处理人像识别结果"""
        self.progress_label.config(text="")

        total_with = len(with_person)
        total_without = len(without_person)

        self.status_var.set(f"人像识别完成：有人 {total_with} 张，无人 {total_without} 张")

        if total_with == 0 and total_without == 0:
            messagebox.showinfo("提示", "未找到可分类的照片。")
            return

        # 创建确认对话框
        result = messagebox.askyesno(
            "分类完成",
            f"人像识别完成！\n\n"
            f"• 有人: {total_with} 张\n"
            f"• 无人: {total_without} 张\n\n"
            f"是否将照片移动到对应文件夹？",
            icon='question'
        )

        if result:
            self._move_person_classified_photos(with_person, without_person)

    def _move_person_classified_photos(self, with_person, without_person):
        """移动已分类的照片到对应文件夹"""
        try:
            # 创建目标文件夹
            person_folder = os.path.join(self.photo_path, "有人")
            no_person_folder = os.path.join(self.photo_path, "无人")

            os.makedirs(person_folder, exist_ok=True)
            os.makedirs(no_person_folder, exist_ok=True)

            # 移动有人的照片
            moved_count = 0
            for photo_path in with_person:
                try:
                    file_name = os.path.basename(photo_path)
                    dest_path = os.path.join(person_folder, file_name)

                    # 如果目标文件已存在，添加序号
                    if os.path.exists(dest_path):
                        base_name, ext = os.path.splitext(file_name)
                        i = 1
                        while os.path.exists(dest_path):
                            dest_path = os.path.join(person_folder, f"{base_name}_{i}{ext}")
                            i += 1

                    shutil.move(photo_path, dest_path)
                    moved_count += 1
                except Exception as e:
                    print(f"移动文件 {photo_path} 失败: {e}")

            # 移动无人的照片
            for photo_path in without_person:
                try:
                    file_name = os.path.basename(photo_path)
                    dest_path = os.path.join(no_person_folder, file_name)

                    # 如果目标文件已存在，添加序号
                    if os.path.exists(dest_path):
                        base_name, ext = os.path.splitext(file_name)
                        i = 1
                        while os.path.exists(dest_path):
                            dest_path = os.path.join(no_person_folder, f"{base_name}_{i}{ext}")
                            i += 1

                    shutil.move(photo_path, dest_path)
                    moved_count += 1
                except Exception as e:
                    print(f"移动文件 {photo_path} 失败: {e}")

            # 更新分类文件夹列表
            if "有人" not in self.category_folders:
                self.category_folders["有人"] = person_folder
            if "无人" not in self.category_folders:
                self.category_folders["无人"] = no_person_folder

            self.update_category_buttons()

            # 重新加载照片列表
            threading.Thread(target=self._load_photos_thread).start()

            messagebox.showinfo(
                "完成",
                f"人像分类完成！\n\n"
                f"已移动 {moved_count} 张照片到对应文件夹。"
            )

        except Exception as e:
            messagebox.showerror("错误", f"移动照片时出错: {str(e)}")

    def _handle_person_classification_error(self, error_message):
        """处理人像识别错误"""
        self.progress_label.config(text="")
        self.status_var.set("人像识别失败")
        messagebox.showerror("错误", f"人像识别失败：{error_message}")


    def _run_baidu_classification(self):
        def progress_callback(current, total, filename):
            self.root.after(0, lambda: self._update_baidu_progress(current, total, filename))

        try:
            classification_result, stats = self.baidu_classifier.classify_files(
                self.photos,
                progress_callback=progress_callback
            )
        except BaiduAPIError as exc:
            self.root.after(0, lambda e=str(exc): self._handle_baidu_failure(e))
            return
        except Exception as exc:
            self.root.after(0, lambda e=str(exc): self._handle_baidu_failure(e))
            return

        self.root.after(
            0,
            lambda result=classification_result, summary=stats: self._handle_baidu_success(result, summary)
        )

    def _update_baidu_progress(self, current, total, filename):
        percent = int((current / total) * 100) if total else 0
        self.progress_label.config(
            text=f"百度识别 {current}/{total} ({percent}%) - {filename}"
        )

    def _handle_baidu_failure(self, message):
        self.progress_label.config(text="")
        self.status_var.set("百度自动分类失败")
        messagebox.showerror("错误", f"百度自动分类失败：{message}")

    def _handle_baidu_success(self, classification_result, stats):
        self.progress_label.config(text="")
        if classification_result:
            result_for_preview = classification_result
            if self.baidu_use_coarse_categories:
                result_for_preview = self.apply_coarse_classification(classification_result)
            classified_count = sum(len(files) for files in result_for_preview.values())
            skipped = 0
            failed = 0
            error_messages = []
            if isinstance(stats, dict):
                skipped = int(stats.get("skipped", 0))
                failed = int(stats.get("failed", 0))
                errors = stats.get("errors") or []
                error_messages = [str(msg) for msg in errors if str(msg).strip()]
            self.status_var.set(
                f"百度识别完成：已分组 {classified_count} 张，低置信度 {skipped} 张，失败 {failed} 张"
            )
            if error_messages:
                preview_errors = "\n".join(error_messages[:5])
                extra = len(error_messages) - 5
                if extra > 0:
                    preview_errors += f"\n... 还有 {extra} 条错误"
                messagebox.showwarning("百度AI部分失败", f"以下文件调用失败：\n{preview_errors}")
            self.show_classification_preview(result_for_preview)
        else:
            self.status_var.set("百度识别未返回有效分类")
            messagebox.showinfo("提示", "百度接口未返回可用的分类结果，请检查阈值或图片质量。")

    def save_baidu_api_config(self):
        """将当前百度配置写回文件"""
        data = {
            "api_key": self.baidu_api_key,
            "secret_key": self.baidu_secret_key,
            "min_score": self.baidu_min_score,
            "allow_unclassified": self.baidu_allow_unclassified,
            "use_coarse_categories": self.baidu_use_coarse_categories,
            "coarse_mapping": self.baidu_coarse_mapping,
        }
        try:
            with open(self.baidu_config_path, "w", encoding="utf-8") as config_file:
                json.dump(data, config_file, ensure_ascii=False, indent=4)
        except Exception as exc:
            print(f"保存百度AI配置失败: {exc}")

    def apply_baidu_min_score(self):
        """应用用户调整的百度置信度阈值"""
        try:
            value = float(self.baidu_min_score_var.get())
        except (tk.TclError, TypeError, ValueError):
            messagebox.showwarning("提示", "请输入0-1之间的数字")
            return
        value = max(0.0, min(1.0, value))
        self.baidu_min_score_var.set(value)
        self.baidu_min_score = value
        if self.baidu_classifier:
            self.baidu_classifier.min_score = value
        self.save_baidu_ai_config_message(f"百度识别最低置信度已更新为 {value:.2f}")

    def toggle_baidu_coarse_mode(self):
        """切换是否启用粗分类"""
        self.baidu_use_coarse_categories = bool(self.baidu_coarse_var.get())
        self.save_baidu_api_config()
        if self.baidu_use_coarse_categories:
            self.status_var.set("百度粗分类已启用")
        else:
            self.status_var.set("百度粗分类已关闭")

    def apply_coarse_classification(self, classification_result):
        """根据配置将细分标签收敛成大类"""
        if not classification_result:
            return classification_result
        mapping = self.baidu_coarse_mapping or {}
        normalized = {
            name: [keyword.lower() for keyword in keywords if keyword]
            for name, keywords in mapping.items()
        }
        aggregated = {}
        for folder, filenames in classification_result.items():
            target = folder
            lower_name = folder.lower()
            for coarse_name, keywords in normalized.items():
                if any(keyword in lower_name for keyword in keywords):
                    target = coarse_name
                    break
            aggregated.setdefault(target, []).extend(filenames)
        return aggregated

    def get_default_coarse_mapping(self):
        """提供粗分类默认配置"""
        return {key: list(values) for key, values in DEFAULT_COARSE_MAPPING.items()}

    def save_baidu_ai_config_message(self, message):
        """保存配置并提示用户"""
        self.save_baidu_api_config()
        self.status_var.set(message)

    def auto_classify_by_name(self):
        """按名字自动分类照片"""
        # 确保已选择照片源文件夹
        if not self.photo_path:
            messagebox.showwarning("警告", "请先加载照片文件夹！")
            return

        # 更新状态
        self.status_var.set("正在分析照片名字，请稍候...")
        self.root.update()

        try:
            # 使用名字分类器进行分类
            classification_result = self.name_classifier.classify_photos(self.photo_path, preview_only=True)

            if not classification_result:
                messagebox.showinfo("提示", "未找到可以按名字分组的照片\n（需要至少2张名字相似的照片才能分组）")
                self.status_var.set("未找到可分组的照片")
                return

            # 显示预览对话框
            self.show_classification_preview(classification_result)

        except Exception as e:
            messagebox.showerror("错误", f"分类过程中发生错误：{str(e)}")
            self.status_var.set("自动分类失败")

    def show_classification_preview(self, classification_result):
        """显示分类预览对话框"""
        # 创建预览窗口
        preview_window = tk.Toplevel(self.root)
        preview_window.title("分类预览")
        preview_window.geometry("700x500")

        # 创建主框架
        main_frame = ttk.Frame(preview_window, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 标题
        title_label = ttk.Label(
            main_frame,
            text=f"共找到 {len(classification_result)} 个分组",
            font=(self.default_font, 12, "bold")
        )
        title_label.pack(pady=(0, 10))

        # 创建滚动文本框显示分类结果
        text_frame = ttk.Frame(main_frame)
        text_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))

        # 使用Text组件替代scrolledtext
        result_text = tk.Text(
            text_frame,
            wrap=tk.WORD,
            font=(self.default_font, 10),
            width=80,
            height=20
        )
        result_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # 添加滚动条
        scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=result_text.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        result_text.configure(yscrollcommand=scrollbar.set)

        # 填充分类结果
        total_photos = 0
        for i, (folder_name, filenames) in enumerate(classification_result.items(), 1):
            result_text.insert(tk.END, f"\n【分组 {i}】文件夹名: {folder_name}\n", "header")
            result_text.insert(tk.END, f"包含 {len(filenames)} 张照片:\n")

            for j, filename in enumerate(filenames[:5], 1):  # 只显示前5个文件名
                result_text.insert(tk.END, f"  {j}. {filename}\n")

            if len(filenames) > 5:
                result_text.insert(tk.END, f"  ... 以及其他 {len(filenames) - 5} 张照片\n")

            result_text.insert(tk.END, "\n")
            total_photos += len(filenames)

        # 配置标签样式
        result_text.tag_config("header", foreground="#5C6BC0", font=(self.default_font, 10, "bold"))

        # 设置为只读
        result_text.configure(state=tk.DISABLED)

        # 相似度调整区域
        settings_frame = ttk.Frame(main_frame)
        settings_frame.pack(fill=tk.X, pady=(0, 10))

        ttk.Label(
            settings_frame,
            text="相似度阈值（越大越严格）:",
            font=(self.default_font, 10)
        ).pack(side=tk.LEFT, padx=(0, 10))

        # 相似度滑块（范围调整为0.8-1.0，更适合新算法）
        similarity_var = tk.DoubleVar(value=self.name_classifier.similarity_threshold)

        similarity_scale = ttk.Scale(
            settings_frame,
            from_=0.80,
            to=1.0,
            orient=tk.HORIZONTAL,
            variable=similarity_var,
            length=200
        )
        similarity_scale.pack(side=tk.LEFT, padx=(0, 10))

        similarity_label = ttk.Label(
            settings_frame,
            text=f"{similarity_var.get():.2f}",
            font=(self.default_font, 10)
        )
        similarity_label.pack(side=tk.LEFT, padx=(0, 10))

        def update_similarity_label(*args):
            similarity_label.configure(text=f"{similarity_var.get():.2f}")

        similarity_var.trace('w', update_similarity_label)

        # 重新分析按钮
        reanalyze_btn = ttk.Button(
            settings_frame,
            text="重新分析",
            bootstyle="info-outline",
            command=lambda: self.reanalyze_classification(preview_window, similarity_var.get())
        )
        reanalyze_btn.pack(side=tk.LEFT)

        # 操作选择
        operation_frame = ttk.Frame(main_frame)
        operation_frame.pack(fill=tk.X, pady=(0, 10))

        operation_var = tk.StringVar(value="move")

        ttk.Radiobutton(
            operation_frame,
            text="移动文件到新文件夹",
            variable=operation_var,
            value="move",
            bootstyle="primary"
        ).pack(side=tk.LEFT, padx=(0, 20))

        ttk.Radiobutton(
            operation_frame,
            text="复制文件到新文件夹",
            variable=operation_var,
            value="copy",
            bootstyle="primary"
        ).pack(side=tk.LEFT)

        # 按钮区域
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X)

        # 确认按钮
        confirm_btn = ttk.Button(
            button_frame,
            text=f"确认分类 ({total_photos}张照片)",
            bootstyle="success",
            command=lambda: self.execute_classification(
                classification_result,
                operation_var.get() == "move",
                preview_window
            )
        )
        confirm_btn.pack(side=tk.LEFT, padx=(0, 10))

        # 取消按钮
        cancel_btn = ttk.Button(
            button_frame,
            text="取消",
            bootstyle="secondary",
            command=preview_window.destroy
        )
        cancel_btn.pack(side=tk.LEFT)

        # 设置窗口为模态
        preview_window.transient(self.root)
        preview_window.grab_set()

    def reanalyze_classification(self, preview_window, new_threshold):
        """使用新的相似度阈值重新分析"""
        preview_window.destroy()
        self.name_classifier.similarity_threshold = new_threshold
        self.auto_classify_by_name()

    def execute_classification(self, classification_result, move_files, preview_window):
        """执行分类操作"""
        preview_window.destroy()

        # 更新状态
        operation_text = "移动" if move_files else "复制"
        self.status_var.set(f"正在{operation_text}文件，请稍候...")
        self.root.update()

        try:
            # 创建文件夹并移动/复制文件
            created_count = self.name_classifier.create_classified_folders(
                self.photo_path,
                classification_result,
                move_files
            )

            # 重新扫描文件夹以更新分类按钮
            self.scan_existing_folders()

            # 如果是移动文件，重新加载照片列表
            if move_files:
                threading.Thread(target=self._load_photos_thread).start()

            # 显示成功消息
            messagebox.showinfo(
                "完成",
                f"成功创建了 {created_count} 个分类文件夹！\n{operation_text}了文件到相应文件夹。"
            )
            self.status_var.set(f"自动分类完成，创建了 {created_count} 个文件夹")

        except Exception as e:
            messagebox.showerror("错误", f"执行分类时发生错误：{str(e)}")
            self.status_var.set("自动分类失败")

    def on_window_resize(self, event):
        """窗口大小变化时的处理"""
        # 只处理来自根窗口的事件
        if event.widget != self.root:
            return

        # 获取窗口宽度
        width = event.width

        # 更新右侧面板宽度
        panel_width = max(int(width / 4), 200)
        self.category_panel.configure(width=panel_width)


def main():
    # 启用DPI感知
    try:
        from ctypes import windll
        windll.shcore.SetProcessDpiAwareness(1)
    except:
        pass  # 如果失败，继续执行

    root = ttk.Window()

    # 设置窗口初始大小
    root.geometry("1100x750")

    # 设置最小窗口大小
    root.minsize(900, 600)

    app = QuickPhotoClassifierApp(root)

    # 更新窗口以确保所有组件都已正确布局
    root.update()

    # 允许窗口调整大小
    root.resizable(True, True)

    root.mainloop()


if __name__ == "__main__":
    main()
